feat(frontend): add access to the imported wallet's signing canister - #13649
Draft
sbpublic wants to merge 3 commits into
Draft
feat(frontend): add access to the imported wallet's signing canister#13649sbpublic wants to merge 3 commits into
sbpublic wants to merge 3 commits into
Conversation
PR3 needs to ask the source wallet's helper canister to sign BTC / EVM / SOL transactions, since those keys are threshold keys rooted in that canister and cannot be derived from the seed phrase. Follows the existing third-party-canister pattern (xtc_ledger, icrc3, …): a build script produces the .did, dfx.json points at it, generate.sh runs it, and the bindings are generated rather than hand-written. One deviation, deliberate: every other script downloads the candid from a public repository, and this interface is published nowhere. The deployed canister exposes it via __get_candid_interface_tmp_hack, which makes the canister the only authoritative source — and fetching from it keeps the checked-in bindings honest about what is actually live, rather than a snapshot transcribed from the extension's minified bundle. The fetch fails the build if the reply does not declare a service, so a truncated response cannot silently yield bindings for an empty interface. Adds plug_helper to the did.delete.types allowlist; without it the generated folder is removed at the end of the pipeline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wraps the four signing methods the remaining chains need — eth, erc20, btc
and sol — following the existing canister/api layering.
Every call passes `{ Sign: null }`. The canister also offers a `Send` mode
that broadcasts for us, deliberately unused: keeping broadcast on our side
means we see the transaction hash and the RPC error directly, and a third
party stays off the critical path for everything but the signature.
Not routed through CanisterApi. That cache holds one actor per principal in
a module-level map that is never evicted, which would pin the imported
identity — and the key derived from the seed phrase — for the lifetime of
the page, outliving both the reset button and the send itself. Sends are
rare enough that building the actor per call costs nothing worth keeping a
secret around for.
The canister reports failures as a plain text variant with no structured
error type, so the text is surfaced as-is rather than mapped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ontend/ic-seed-phrase-import-chains
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
PR1 displays what an imported wallet holds; PR2 moves the ICP and ICRC balances. The remaining chains — BTC, EVM, SOL — cannot be moved the same way, because their keys are not derived from the seed phrase at all.
They are chain-key (threshold) keys rooted in the source wallet's own helper canister. On the management canister,
ecdsa_public_keyandschnorr_public_keyaccept acanister_idargument, butsign_with_ecdsaandsign_with_schnorrdo not — the key is derived from the caller. So those addresses are readable by anyone (which is how PR1 derives them offline) and signable only by that canister.This PR adds the access layer for asking it to sign. No user-visible change; the per-chain send flows follow in their own PRs, one per chain.
Stacked on #13648; review that first. This PR targets its branch.
Changes
xtc_ledger,icrc3, …): a build script produces the.did,dfx.jsonpoints at it,generate.shruns it, and the bindings are generated rather than hand-written.send_eth,send_erc20,send_btc,sign_sol.Four decisions worth review attention:
__get_candid_interface_tmp_hack, which makes the deployed canister the only authoritative source — and fetching from it keeps the checked-in bindings honest about what is live, rather than a snapshot transcribed from the extension's minified bundle. The fetch fails the build if the reply does not declare a service, so a truncated response cannot silently produce bindings for an empty interface.{ Sign: null }everywhere, neverSend. The canister will broadcast for us; we don't let it. Broadcasting ourselves means we see the transaction hash and the RPC error directly, and a third party stays off the critical path for everything but the signature.CanisterApi. That cache holds one actor per principal in a module-level map that is never evicted, which would pin the imported identity — and the key derived from the seed phrase — for the lifetime of the page, outliving both the reset button and the send itself. Sends are rare enough that building the actor per call costs nothing worth keeping a secret around for.plug_helperadded to thedid.delete.typesallowlist. That script deletes any generated folder not named in a hardcoded list; without the entry the bindings are removed at the end of the pipeline.This touches
dfx.jsonandscripts/, whichAGENTS.mdmarks as restricted — done with explicit approval, and following the established pattern rather than inventing one.Tests
New
plug-helper.canister.spec.ts(9 cases): each method returns its payload, throws the canister's error text onErr, and — for eth and erc20 — is asserted to pass arguments in the canister's exact positional order and to request{ Sign: null }rather thanSend. The positional assertions are the valuable ones: the interface is all unnamednatparameters, so a transposed gas and nonce would type-check and fail only on chain.Local gates, all green:
npm run formatnpm run lint -- --max-warnings 0(whole repo)npm run checknpm run check:testsvitestRegenerating exposed one wrinkle worth recording:
did.update.types.mjsrewrites 15 unrelated*.factory.certified.did.jsfiles with a cosmetic trailing comma, which the pipeline's finalnpm run formatcleans up. Running the steps by hand without that final format would have committed 15 files of churn; the diff here touches none of them.Not verified against the live canister.
eth_addressconfirmed the canister accepts callers other than the source wallet, but no signing method has been called by an outside caller. That is the main risk in this stack and is tested in the EVM PR, which is the first to exercise a real signature.🤖 Generated with Claude Code